iT邦幫忙

2023 iThome 鐵人賽

DAY 2
1

初翼大綱

接下來,連續三天,我們會每天分享五個Python小技巧。

1. default判斷語法

# 01a中,x or 'x'使用的原理為bool(None)會被視為False,所以當沒有給定預定值時,self.x會被設定為'x',算是一種常用的語法。但這麼一來,當x給定任何布林值為Falseobj時,Python會使用or之後的'x'來作為self.x,包括NoneFalse和空的container等等。

# 01a
class MyClass:
    def __init__(self, x=None):
        self.x = x or 'x'


if __name__ == '__main__':
    my_inst = MyClass(False)
    print(my_inst.x)  # 'x' (not False)

當不確定參數是否會傳入布林值為Falseobj時,我們建議使用# 01b中,顯性判斷其是否為None的語法。看起來比較不酷沒錯,但是想起那些踩坑時的痛苦,建議還是多打幾個字,畢竟The Zen of Python都告誡我們Explicit is better than implicit.

# 01b
class MyClass:
    def __init__(self, x=None):
        self.x = x if x is not None else 'x'


if __name__ == '__main__':
    my_inst = MyClass(False)
    print(my_inst.x)  # False

2. object()作為預設值

None是一個不錯的預設值。但當您的程式可以接受None為一合法輸入值時,又該以什麼值來當預設值呢?事實上,不管你設什麼樣的值,使用者都可能會直接傳入那個值。

一個常見的方法是使用object()來當預設值,一般我們會稱呼這樣的用法為sentinel。由於每次的sentinel是無法預測的,所以除非於寫code時,顯性傳進這個值,才能確定這個值是由自己傳入的。

假設我們想要寫一個get_given function,其要求如下:

  • 接受兩個參數givendefault
  • 當顯性給予given時,該function需回傳given,否則即回傳default,而default之預設值為0

# 02a中以Nonegiven的預設值,此時當顯性給予None時,其仍然會回傳2,但這不符合我們所希望的行為。

# 02a
def get_given(given=None, default=0):
    return default if given is None else given


if __name__ == '__main__':
    print(get_given('abc'))  # 'abc'
    print(get_given(None, 2))  # 2

# 02b中以object()sentinel來作為given的預設值,此時只有當顯性傳入sentinelgiven時,才能打破回傳值不是給予值的情況。

# 02b
sentinel = object()


def get_given(given=sentinel, default=0):
    return default if given is sentinel else given


if __name__ == '__main__':
    print(get_given('abc'))  # 'abc'
    print(get_given(None, 2))  # None
    # We can only get 2(default) by passing sentinel to given explicitly
    print(get_given(sentinel, 2))  # 2

3. tuple作為預設值

在建立class的時候,常常會有一種情況是希望於__init__中選擇性接收一些值,來更新內部的某個mutablecontainer,像是dictsetlist等。

舉例來說,# 03a中的MyDict繼承UserDict註1)。於__init__中接收一個選擇性的dict_data。當其不為None時,我們希望能將傳入的值,更新到self

# 03a
from collections import UserDict


class MyDict(UserDict):
    def __init__(self, dict_data=None):
        super().__init__()
        if dict_data is not None:
            self.update(dict_data)

由於這樣類似的pattern很常出現,我們就開始動動腦,是不是有辦法免除這個if的確認呢?

首先# 03b試著將dict_data設為{}。修但幾勒,我們了解大家會說你怎麼可以把mutableobj作為預設值呢?難道你沒讀過Python人必看的The Hitchhiker's Guide to Python嗎?但仔細想想,如果沒有設一個instance variabledict_data的話,我們「好像」沒有辦法mutate它(除非直接mutate dict_data)。

# 03b
from collections import UserDict


class MyDict(UserDict):
    def __init__(self, dict_data={}):
        super().__init__()
        self.update(dict_data)

當然,如果您寫成# 03c的格式,那麼我們就可以藉由self._dict_datamutate dict_data。變動d._dict_data也會變動d2._dict_data,因為兩個是同一個obj,這樣的行為相信不是您想要的。

# 03c
from collections import UserDict


class MyDict(UserDict):
    def __init__(self, dict_data={}):
        super().__init__()
        self._dict_data = dict_data
        self.update(dict_data)


if __name__ == '__main__':
    d, d2 = MyDict(), MyDict()
    print(d._dict_data is d2._dict_data)  # True
    print(d, d2)  # {}, {}
    d._dict_data['a'] = 1
    print(d._dict_data is d2._dict_data)  # True
    print(d._dict_data, d2._dict_data)  # {'a': 1} {'a': 1}

# 03d中,使用空tuple來當預設值,由於tupleimmutable,所以不會有# 03c的情況,是一個我們覺得可以考慮的寫法,尤其是當這個變數代表iterable時。

# 03d
from collections import UserDict


class MyDict(UserDict):
    def __init__(self, dict_data=()):
        super().__init__()
        self._dict_data = dict_data
        self.update(dict_data)


if __name__ == '__main__':
    d, d2 = MyDict(), MyDict()
    print(d._dict_data is d2._dict_data)  # True
    print(d, d2)  # {}, {}
    d._dict_data = (1,)
    print(d._dict_data is d2._dict_data)  # False
    print(d._dict_data, d2._dict_data)  # (1,) ()

4. datetime.strftime vs f-string

當需要formatdatetimeobj時,一般會使用datetime.strftime,如# 04a

# 04a
from datetime import datetime

now = datetime.now()
datetime_fmt = '%Y-%m-%d_%H:%M:%S'

if __name__ == '__main__':
    now_str = now.strftime(datetime_fmt)
    print(f'{now_str}')  # 2023-09-01_21:14:41

但其實f-string是可以認得datetime format所用的格式,而且使用起來更為方便。# 04b最後一行的f-string,我們使用兩層{}。外層datetime object:後,可擺入需要format的格式。

# 04b
from datetime import datetime

now = datetime.now()
datetime_fmt = '%Y-%m-%d_%H:%M:%S'

if __name__ == '__main__':
    print(f'{now:{datetime_fmt}}')  # 2023-09-01_21:14:41

5. 以docstrings代替pass

當定義客製的Exception時,常使用pass,如# 05a

# 05a
class MyError(Exception):
    pass

其實有時候也可以考慮使用docstrings代替pass,如# 05b。除了這是個合法的語法外(相當於指定__doc__),也可以為Exception增加說明。其它像是定義需要略為說明的class或是function時,也可以使用。

# 05b
class BetterMyError(Exception):
    """This error will be raised if..."""

另外,一個有趣的小知識,其實Ellipsis也是合法的語法,如# 05c

# 05c
class MyCoolError(Exception):
    ...

備註

註1:UserDict相比於繼承dict更加容易操作,如果有興趣深入研究的朋友,可以參考Trey的說明或是Python docs

Code

本日程式碼傳送門


上一篇
[Day01] 緣起及Python學習資源分享
下一篇
[Day03] 初翼 - Tips:6~10
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言